package persistencia;

/**
 * Una clase para representar un archivo binario, cuyo contenido son registros uniformes
 * (del mismo tipo) y de la misma longitud. El archivo no permite grabar objetos cuyo tipo
 * y tama�o no coincida con los que se indicaron en el constructor.
 * @author Ing. Valerio Frittelli
 * @version Julio de 2005
 */
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.net.URISyntaxException;

import config.ConfigManager;
import exception.InvalidConfigException;

public class Archivo {
	private File fd; // descriptor del archivo para acceder a sus propiedades
	// externas
	private RandomAccessFile maestro; // objeto para acceder al contenido del
	// archivo
	private Grabable tipo; // representa al contenido grabado en el archivo
	private Register reg; // auxiliar para operaciones internas
	private String nombreArchivoTemporal;

	/**
	 * Crea un manejador para el Archivo, asociando al mismo un nombre a modo de file
	 * descriptor, y un tipo de contenido al que quedar� fijo. El segundo par�metro se usa
	 * para fijar el tipo de registro que ser� aceptado para grabar en el archivo. No se
	 * crea el archivo en disco, ni se abre. S�lo se crea un descriptor general para �l. La
	 * apertura y eventual creaci�n, debe hacerse con el m�todo openForReadWrite().
	 * @param nombre es el nombre f�sico del archivo a crear
	 * @param r una instancia de la clase a la que pertenecen los objetos cuyos datos ser�n
	 * grabados. La instancia referida por r NO ser� grabada en el archivo
	 * @throws ClassNotFoundException si no se informa correctamente el tipo de registro a
	 * grabar
	 */
	public Archivo(String nombre, Grabable r) throws ClassNotFoundException, IOException {
		if (r == null)
			throw new ClassNotFoundException("Clase incorrecta o inexistente para el tipo de registro");
		tipo = r;
		reg = new Register(r);
		try {
			fd = new File(this.getFilesPath() + nombre);
		} catch (URISyntaxException e) {
			e.printStackTrace();
			throw new IOException(e);
		}
		nombreArchivoTemporal = setNombreArchivoTemporal();
	}

	/**
	 * Acceso al descriptor del archivo
	 * @return un objeto de tipo File con las propiedades de file system del archivo
	 */
	public File getFileDescriptor() {
		return fd;
	}

	/**
	 * Acceso al manejador del archivo binario
	 * @return un objeto de tipo RandomAccessFile que permite acceder al bloque f�sico de
	 * datos en disco, en forma directa
	 */
	public RandomAccessFile getMasterFile() {
		return maestro;
	}

	/**
	 * Acceso al descriptor de la clase del registro que se graba en el archivo
	 * @return una cadena con el nombre de la clase del registro usado en el archivo
	 */
	public String getRegisterType() {
		return tipo.getClass().getName();
	}

	/**
	 * Borra el Archivo del disco
	 */
	public void delete() {
		fd.delete();
	}

	/**
	 * Cambia el nombre del archivo
	 * @param nuevo otro Archivo, cuyo nombre (o file descriptor) ser� dado al actual
	 */
	public void rename(Archivo nuevo) {
		fd.renameTo(nuevo.fd);
	}

	/**
	 * Abre el archivo en modo de s�lo lectura. El archivo en disco debe existir
	 * previamente. Queda posicionado al principio del archivo.
	 */
	public void openForRead() throws IOException {
		/*
		 * try {
		 */
		maestro = new RandomAccessFile(fd, "r");
		/*
		 * } catch(IOException e) { System.out.println("Error de apertura archivo
		 * " + fd.getName() + ": " + e.getMessage()); //System.exit(1); }
		 */
	}

	/**
	 * Abre el archivo en modo de lectura y grabaci�n. Si el archivo en disco no exist�a,
	 * ser� creado. Si exist�a, ser� posicionado al principio del archivo. Mueva el puntero
	 * de registro activo con el m�todo seekRegister() o con seekByte().
	 */
	public void openForReadWrite() throws IOException {
		/*
		 * try {
		 */
		maestro = new RandomAccessFile(fd, "rw");
		/*
		 * } catch(IOException e) { System.out.println("Error de apertura archivo
		 * " + fd.getName() + ": " + e.getMessage()); //System.exit(1); }
		 */
	}

	/**
	 * Cierra el archivo
	 */
	public void close() throws IOException {
		/*
		 * try {
		 */
		maestro.close();
		/*
		 * } catch(IOException e) { System.out.println("Error al cerrar el archivo
		 * " + fd.getName() + ": " + e.getMessage()); //System.exit(1); }
		 */
	}

	/**
	 * Ubica el puntero de registro activo en la posici�n del registro n�mero i. Se supone
	 * que los registros grabados son del mismo tipo, y que la longitud de los registros es
	 * uniforme.
	 * @param i n�mero relativo del registro que se quiere acceder
	 */
	public void seekRegister(long i) throws IOException {
		/*
		 * try {
		 */
		maestro.seek(i * reg.sizeOf());
		/*
		 * } catch(IOException e) { System.out.println("Error al posicionar en el registro
		 * n�mero " + i + ": " + e.getMessage()); //System.exit(1); }
		 */
	}

	/**
	 * Ubica el puntero de registro activo en la posici�n del byte n�mero b
	 * @param b n�mero del byte que se quiere acceder, contando desde el principio del
	 * archivo
	 * @throws IOException si hubo problema en el posicionamiento
	 */
	public void seekByte(long b) throws IOException {
		/*
		 * try {
		 */
		maestro.seek(b);
		/*
		 * } catch(IOException e) { System.out.println("Error al posicionar en el byte
		 * n�mero " + b + ": " + e.getMessage()); //System.exit(1); }
		 */
	}

	/**
	 * Rebobina el archivo: ubica el puntero de registro activo en la posici�n cero
	 */
	public void rewind() throws IOException {
		/*
		 * try {
		 */
		maestro.seek(0);
		/*
		 * } catch(IOException e) { System.out.println("Error al rebobinar el archivo: " +
		 * e.getMessage()); //System.exit(1); }
		 */
	}

	/**
	 * Devuelve el n�mero relativo del registro en el cual esta posicionado el archivo en
	 * este momento
	 * @return el n�mero del registro actual
	 */
	public long registerPos() {
		try {
			return maestro.getFilePointer() / reg.sizeOf();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("Error al intentar devolver el n�mero de registro: " + e.getMessage());
			// System.exit(1);
		}
		return -1;
	}

	/**
	 * Devuelve el n�mero de byte en el cual esta posicionado el archivo en este momento
	 * @return el n�mero de byte de posicionamiento actual
	 */
	public long bytePos() {
		try {
			return maestro.getFilePointer();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("Error al intentar devolver el n�mero de byte: " + e.getMessage());
			// System.exit(1);
		}
		return -1;
	}

	/**
	 * Posiciona el puntero de registro activo al final del archivo
	 */
	public void goFinal() throws IOException {
		/*
		 * try {
		 */
		maestro.seek(maestro.length());
		/*
		 * } catch(IOException e) { System.out.println("Error al posicionar al final: " +
		 * e.getMessage()); //System.exit(1); }
		 */
	}

	/**
	 * Devuelve la cantidad de registros del archivo en este momento
	 * @return el n�mero de registros del archivo
	 */
	public long countRegisters() {
		try {
			return maestro.length() / reg.sizeOf();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("Error al calcular el n�mero de registros: " + e.getMessage());
			// System.exit(1);
		}
		return 0;
	}

	/**
	 * Determina si se ha llegado al final del archivo o no
	 * @return true si se lleg� al final - false en caso contrario
	 * @throws IOException si hubo problema en la operaci�n
	 */
	public boolean eof() {
		try {
			return !(maestro.getFilePointer() < maestro.length());
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("Error al determinar el fin de archivo: " + e.getMessage());
			// System.exit(1);
		}
		return true;
	}

	/**
	 * Graba un registro en el archivo
	 * @param r el registro a grabar
	 */
	public void grabar(Register r) throws Exception {
		if (r != null && r.getData().getClass() == tipo.getClass()) {
			/*
			 * try {
			 */
			r.grabar(maestro);
			/*
			 * } catch(Exception e) { System.out.println("Error al grabar el registro: " +
			 * e.getMessage()); //System.exit(1); }
			 */
		}
	}

	/**
	 * Lee un registro del archivo
	 * @return el registro leido
	 * @throws IOException si hubo problema en la operaci�n
	 */
	public Register leer() {
		Register r = null;
		try {
			r = new Register((Grabable) tipo.getClass().newInstance());
			r.leer(maestro);
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("Error al leer el registro: " + e.getMessage());
			// System.exit(1);
		}
		return r;
	}

	/**
	 * Busca un registro en el archivo. Si la clase del registro que se busca no coincide
	 * con la clase de los registros grabados en el archivo, retorna -1. En general, el
	 * retorno de -1 significa que el registro no fue encontrado.
	 * @param r objeto a buscar en el archivo
	 * @return la posici�n de byte del registro en el archivo, si existe, o -1 si no existe
	 */
	public long buscar(Register r) {
		if (r == null || tipo.getClass() != r.getData().getClass())
			return -1;
		long pos = -1, actual = bytePos();
		try {
			rewind();
			while (!eof()) {
				reg = leer();
				if (reg.getData().equals(r.getData()) && reg.isActive()) {
					pos = bytePos() - reg.sizeOf();
					break;
				}
			}
			seekByte(actual);
		} catch (IOException ex) {
			ex.printStackTrace();
			System.out.println("Error al buscar el registro: " + ex.getMessage());
		}
		return pos;
	}

	/**
	 * Agrega un registro en el archivo, controlando que no haya repetici�n y que la clase
	 * del nuevo registro coincida con la clase indicada para el archivo al invocar al
	 * constructor. El archivo debe estar abierto en modo de grabaci�n.
	 * @param r registro a agregar
	 * @return true si fue posible agregar el registro - false si no fue posible
	 */
	public boolean alta(Register r) {
		boolean resp = false;
		long pos;
		if (r != null && tipo.getClass() == r.getData().getClass()) {
			try {
				openForReadWrite();
				pos = buscar(r);
				if (pos == -1) {
					goFinal();
					grabar(r);
					resp = true;
				}
				close();
			} catch (IOException ex) {
				System.out.println("Error al intentar acceder al archivo: " + ex.getMessage());
			} catch (Exception e) {
				System.out.println("Error al grabar el registro: " + e.getMessage());
				// System.exit(1);
			}
		}
		return resp;
	}

	/**
	 * Agrega un registro en el archivo, sin controlar repetici�n. La clase del nuevo
	 * registro debe coincidir con la clase indicada para el archivo al invocar al
	 * constructor. El archivo debe estar abierto en modo de grabaci�n.
	 * @param r registro a agregar
	 * @return true si fue posible agregar el registro - false si no fue posible
	 */
	public boolean altaDirecta(Register r) {
		boolean resp = false;
		if (r != null && tipo.getClass() == r.getData().getClass()) {
			try {
				openForReadWrite();
				goFinal();
				grabar(r);
				resp = true;
				close();
			} catch (IOException ex) {
				System.out.println("Error al intentar acceder al archivo: " + ex.getMessage());
			} catch (Exception e) {
				System.out.println("Error al grabar el registro: " + e.getMessage());
				// System.exit(1);
			}
		}
		return resp;
	}

	/**
	 * Borra un registro del archivo. La clase del registro buscado debe coincidir con la
	 * clase indicada para el archivo al invocar al constructor. El archivo debe estar
	 * abierto en modo de grabaci�n. El registro se marca como borrado, aunque sigue
	 * f�sicamente ocupando lugar en el archivo
	 * @param r registro a buscar y borrar
	 * @return true si fue posible borrar el registro - false si no fue posible
	 */
	public boolean baja(Register r) {
		boolean resp = false;
		long pos;
		if (r != null && tipo.getClass() == r.getData().getClass()) {
			try {
				openForReadWrite();
				pos = buscar(r);
				if (pos != -1) {
					seekByte(pos);
					reg = leer();
					reg.setActive(false);
					seekByte(pos);
					grabar(reg);
					resp = true;
				}
				close();
			} catch (IOException ex) {
				System.out.println("Error al intentar acceder al archivo: " + ex.getMessage());
			} catch (Exception e) {
				System.out.println("Error al eliminar el registro: " + e.getMessage());
				// System.exit(1);
			}
		}
		return resp;
	}

	public boolean baja(long i) {
		boolean resp = false;
		try {
			openForReadWrite();
			seekRegister(i);
			reg = leer();
			reg.setActive(false);
			seekRegister(i);
			grabar(reg);
			resp = true;
			close();
		} catch (IOException ex) {
			System.out.println("Error al intentar acceder al archivo: " + ex.getMessage());
		} catch (Exception e) {
			System.out.println("Error al eliminar el registro: " + e.getMessage());
			// System.exit(1);
		}
		return resp;
	}

	/**
	 * Modifica un registro en el archivo. Reemplaza el registro en una posici�n dada, por
	 * otro tomado como par�metro. La clase del registro buscado debe coincidir con la
	 * clase indicada para el archivo al invocar al constructor. El archivo debe estar
	 * abierto en modo de grabaci�n.
	 * @param r registro con los nuevos datos
	 * @return true si fue posible modificar el registro - false si no fue posible
	 */
	public boolean modificar(Register r) {
		boolean resp = false;
		long pos;
		if (r != null && tipo.getClass() == r.getData().getClass()) {
			try {
				openForReadWrite();
				pos = buscar(r);
				if (pos != -1) {
					seekByte(pos);
					grabar(r); // graba el nuevo registro encima del anterior
					resp = true;
				}
				close();
			} catch (IOException ex) {
				System.out.println("Error al intentar acceder al archivo: " + ex.getMessage());
			} catch (Exception e) {
				System.out.println("Error al modificar el registro: " + e.getMessage());
				// System.exit(1);
			}
		}
		return resp;
	}

	/**
	 * Elimina f�sicamente los registros que estuvieran marcados como borrados. El Archivo
	 * queda limpio, pero sale cerrado.
	 */
	public void cleanupFile() throws ClassNotFoundException, IOException, Exception {
		/*
		 * try {
		 */
		Archivo temp = new Archivo(this.nombreArchivoTemporal, tipo);
		temp.openForReadWrite();
		this.openForRead();
		while (!this.eof()) {
			reg = this.leer();
			if (reg.isActive())
				temp.grabar(reg);
		}
		this.close();
		temp.close();
		this.delete();
		temp.rename(this);
		/*
		 * } catch (ClassNotFoundException e) { System.out
		 * .println("Error de tipo de dato con el archivo temporal: " + e.getMessage()); //
		 * System.exit(1); }
		 */
	}

	public void ordenar() throws IOException, Exception {
		long i, j, n;
		Register ri, rj;
		openForReadWrite();
		n = countRegisters();
		for (i = 0; i < n - 1; i++) {
			// seekRegister(i); // esta no hace falta...
			ri = leer();
			for (j = i + 1; j < n; j++) {
				// seekRegister(j); // esta no hace falta...
				rj = leer();
				if (ri.compareTo(rj) > 0) {
					seekRegister(j);
					grabar(ri); // s�... ri
					ri = rj;
				}
			}
			seekRegister(i);
			grabar(ri);
		}
		close();
	}

	/**
	 * Toma el nombre de archivo temporal, y su ruta, desde un archivo de configuracion
	 */
	private String setNombreArchivoTemporal() {
		String nombreArchivo = "";
		;
		try {
			nombreArchivo = ConfigManager.getStringConfig("temp_file_name");
		} catch (InvalidConfigException ex) {
			System.out.println("Archivo Temporal: " + ex.getMessage());
		}
		return nombreArchivo;
	}

	/**
	 * Lee el path seteado en un archivo de configuracion, y lo devuelve
	 * @return path, el path del archivo
	 * @throws URISyntaxException 
	 */
	private String getFilesPath() throws URISyntaxException {
		String path = "";
		try {
			path = ConfigManager.getStringConfig("application_files_path");
			if (ConfigManager.getBasePath() != null) {
				path = ConfigManager.getBasePath() + path.replace("src/", "");
			}
		} catch (InvalidConfigException ex) {
			System.out.println("Error obteniendo el path: " + ex.getMessage());
		}
		return new URI(path).getPath();
	}
}